<?php

/**
 * @author Michael Thomas
 * @copyright 2008
 */

require_once ('include_config_login.php');
require_once ('include_config_mysql.php');

class Login
{
  /* Constants for each authorization mode */
  const AUTH_MODE_DB = 0;
  const AUTH_MODE_UNIX = 1;

  /* Prefix for extra user data, to prevent conflicting with required values */
  const INFO_PREFIX = 'info-';

  /*
    The name of the username session/cookie variable.
  */
  const VAR_USERNAME = 'username';

  /*
    The name of the authkey session/cookie variable.
  */
  const VAR_AUTHKEY = 'authkey';

  const DEBUG = false;

  /*
    An associative array of user data from a mysql table as specified in the
    configuration.
  */
  public $userData = array();

  /* The name of the current user, for public consumption */
  private $userName;

  /*
    This contains a hash of the user's password hash. This is to prevent users
    from simply changing their username session variable to gain privileges.
  */
  private $authKey;

  /* Whether or not the user is logged in */
  private $isLoggedIn = false;

  /* Config data loaded from the config file */
  private $config;

  private $debugMessages = array();

  /* The Singleton */
  private static $instance;

  /*
    ----------------------------------------------------------------------------
    Public Methods
    ----------------------------------------------------------------------------
  */

  public function doLogin($username, $password, $remember = false)
  {
    if ($this->authenticate($username, $password)) {
      $this->createSession();
      if ($remember) {
        $this->createCookies();
      }
      $this->isLoggedIn = true;
    } else {
      $this->isLoggedIn = false;
    }
    if ($this->isLoggedIn) {
      $this->loadUserData();
    }
    return $this->isLoggedIn;
  }

  public function doLogout()
  {
    $this->destroySession();
    $this->destroyCookies();
    $this->isLoggedIn = false;
  }

  public function doDummyLogin()
  {
    $this->isLoggedIn = true;
    $this->userName = 'dummy';    
  }

  public static function getUserName()
  {
    $login = Login::getInstance();
    return $login->userName;
  }

  public static function isLoggedIn()
  {
    $login = Login::getInstance();
    return $login->isLoggedIn;
  }

  /* Making this static so you can require a login with 1 line instead of 2 */
  public static function requireLogin()
  {
    $login = self::getInstance();
    if (!$login->isLoggedIn) {
      $login->redirectToLogin('You must login to view that page');
    }
  }

  public static function requireProperty($name, $value)
  {
    $login = self::getInstance();
    $propertyExists = isset($login->userData[$name]);
    if (is_array($value)) {
      $propertyMatches = in_array($login->userData[$name], $value);
    } else {
      $propertyMatches = ($login->userData[$name] == $value);
    }
    if (!$propertyExists || !$propertyMatches) {
      $login->redirectToLogin('You do not have permission to access that page');
    }
  }

  protected function redirectToLogin($message)
  {
    Util::setSessionMessage($message);
    $url = $this->config['LOGIN_PAGE'] . '?' .
        $this->config['REDIRECT_PARAM'] .
        '=' . urlencode(Util::getCurrentUrl());
    header('Location: ' . $url);
    exit();
  }

  /*
    ----------------------------------------------------------------------------
    Cookie / Session
    ----------------------------------------------------------------------------
  */

  private function resumeSession()
  {
    $hasSessionCookie = isset($_COOKIE[session_name()]);
    $hasRememberCookies = isset($_COOKIE[self::VAR_USERNAME]) &&
        isset($_COOKIE[self::VAR_AUTHKEY]);
    if ($hasSessionCookie || $hasRememberCookies) {
      if ($this->restoreFromSession() && $this->reauthenticate()) {
        $this->isLoggedIn = true;
      } elseif ($this->restoreFromCookies() && $this->reauthenticate()) {
        $this->createSession();
        $this->isLoggedIn = true;
      } else {
        $this->isLoggedIn = false;
      }
    }
    if ($this->isLoggedIn) {
      $this->loadUserData();
    }
  }

  private function createSession()
  {
    $this->destroySession();
    session_start();
    $_SESSION[self::VAR_USERNAME] = $this->userName;
    $_SESSION[self::VAR_AUTHKEY] = $this->authKey;
    $this->debug('Created session');
  }

  private function createCookies()
  {
    $expireTime = time() + 3600 * 24 * $this->config['REMEMBER_TIME'];
    setcookie(self::VAR_USERNAME, $this->userName, $expireTime);
    setcookie(self::VAR_AUTHKEY, $this->authKey, $expireTime);
    $this->debug('Created cookies');
  }

  private function restoreFromSession()
  {
    session_start();
    if (isset($_SESSION[self::VAR_USERNAME]) &&
        isset($_SESSION[self::VAR_AUTHKEY])) {
      $this->userName = $_SESSION[self::VAR_USERNAME];
      $this->authKey = $_SESSION[self::VAR_AUTHKEY];
      $this->debug('Session variables found');
      return true;
    }
    $this->debug('Session variables not found');
    return false;
  }

  private function restoreFromCookies()
  {
    if (isset($_COOKIE[self::VAR_USERNAME]) &&
        isset($_COOKIE[self::VAR_AUTHKEY])) {
      $this->userName = $_COOKIE[self::VAR_USERNAME];
      $this->authKey = $_COOKIE[self::VAR_AUTHKEY];
      $this->debug('Cookies found');
      return true;
    }
    $this->debug('Cookies not found');
    return false;
  }

  private function destroySession()
  {
    if (isset($_COOKIE[session_name()])) {
      setcookie(session_name(), '', time() - 60000, '/');
    }
    @session_destroy();
    $this->debug('Session destroyed');
  }

  private function destroyCookies()
  {
    $expireTime = time() - 60000;
    setcookie(self::VAR_USERNAME, '', $expireTime);
    setcookie(self::VAR_AUTHKEY, '', $expireTime);
    $this->debug('Cookies destroyed');
  }

  /*
    ----------------------------------------------------------------------------
    Authentication
    ----------------------------------------------------------------------------
  */

  /*
    Validate a given username and password.
  */
  private function authenticate($username, $password)
  {
    $realCryptPass = $this->getRealCryptPassword($username);
    if (!$realCryptPass) {
      return false;
    }
    if (testPassword($password, $realCryptPass)) {
      $this->userName = $username;
      $this->authKey = $this->getAuthKey($realCryptPass);
      $this->debug('Authentication Successful');
      $this->debug('Password: ' . $realCryptPass);
      $this->debug('AuthKey: ' . $this->authKey);
      return true;
    }
    $this->debug('Authentication Failed');
    return false;
  }

  /*
    Validate an authKey. The authKey is retrieved from either a session or
    cookies prior to this method being called.
  */
  private function reauthenticate()
  {
    $realCryptPass = $this->getRealCryptPassword($this->userName);
    if (!$realCryptPass) {
      return false;
    }

    $testAuthKey = $this->getAuthKey($realCryptPass);
    if ($this->authKey === $testAuthKey) {
      $this->debug('UNIX reauthentication successful');
      return true;
    }
    $this->debug('UNIX reauthentication failed');
    $this->debug("$this->authKey !== $testAuthKey");
    return false;
  }

  private function getUnixHash($username)
  {
    $userInfo = posix_getpwnam($username);
    if ($userInfo) {
      return $userInfo['passwd'];
    }
    return false;
  }

  private function getDBHash($username)
  {
    $username = Util::getMysqlSafeString($username);
    $result = Util::query("SELECT {$this->config['USER_INFO_PASSWORD_COLUMN']}
                           FROM `{$this->config['USER_INFO_TABLE']}`
                           WHERE `{$this->config['USER_INFO_USERNAME_COLUMN']}`
                           LIKE '$username'");
    if (mysql_num_rows($result) === 1) {
      $data = mysql_fetch_array($result, MYSQL_NUM);
      return $data[0];
    }
    return false;
  }

  private function getRealCryptPassword($username)
  {
    if ($this->config['AUTH_MODE'] == self::AUTH_MODE_UNIX) {
      $realCryptPass = $this->getUnixHash($username);
    } elseif ($this->config['AUTH_MODE'] == self::AUTH_MODE_DB) {
      $realCryptPass = $this->getDBHash($username);
    }
    return $realCryptPass;
  }

  /*
    Creates and returns a hash of the users password hash and the IP address
    they used to log in. This is to prevent someone from stealing their
    session/cookies.
  */
  private function getAuthKey($realCryptPass)
  {
    $authKey = crypt($realCryptPass . $_SERVER['REMOTE_ADDR'], $realCryptPass);
    return $authKey;
  }

  /*
    ----------------------------------------------------------------------------
    Miscellaneous
    ----------------------------------------------------------------------------
  */

  private function debug($message)
  {
    array_push($this->debugMessages, $message);
  }

  public function printDebugMessages()
  {
    echo '<pre>' . implode("\n", $this->debugMessages) . '</pre>';
  }

  private function loadConfiguration()
  {
    global $loginConfig;
    $this->config = $loginConfig;

    if (!isset($this->config['REMEMBER_TIME']) ||
        !is_numeric($this->config['REMEMBER_TIME'])) {
      throw new LogicException('REMEMBER_TIME is invalid');
    }

    /* Verify all the necessary values are set for DB auth mode */
    if ($this->config['AUTH_MODE'] == self::AUTH_MODE_DB) {

      $requiredValues = array('USER_INFO_TABLE', 'USER_INFO_DATABASE',
          'USER_INFO_USERNAME_COLUMN', 'USER_INFO_PASSWORD_COLUMN');

      foreach ($requiredValues as $value) {
        if (!isset($this->config[$value]) || empty($this->config[$value])) {
          throw new LogicException($value .
              ' must be set in the config file to use database authentication');
        }
      }
    }

    /*
      If user info fields are specified, make sure the rest of the table data
      is as well.
    */
    if (isset($this->config['USER_INFO_FIELDS']) &&
        !empty($this->config['USER_INFO_FIELDS'])) {

      $requiredValues = array('USER_INFO_TABLE', 'USER_INFO_DATABASE',
          'USER_INFO_USERNAME_COLUMN');

      foreach ($requiredValues as $value) {
        if (!isset($this->config[$value]) || empty($this->config[$value])) {
          throw new LogicException($value .
              ' must be set in the config file to get user info fields');
        }
      }

    }
  }

  private function loadUserData()
  {
    if (!isset($this->config['USER_INFO_FIELDS']) ||
        empty($this->config['USER_INFO_FIELDS'])) {
      return;
    }
    $fields = implode(', ', array_keys($this->config['USER_INFO_FIELDS']));
    $table = $this->config['USER_INFO_TABLE'];
    $database = $this->config['USER_INFO_DATABASE'];
    $usernameColumn = $this->config['USER_INFO_USERNAME_COLUMN']; 

    $result = mysql_query("SELECT $fields
                           FROM $database.$table
                           WHERE $usernameColumn = '$this->userName'
                           LIMIT 1" 
                          ) or die(mysql_error());

    $data = mysql_fetch_array($result, MYSQL_ASSOC);
    foreach ($data as $key => $value) {
      $this->userData[$this->config['USER_INFO_FIELDS'][$key]] = $value;
    }
  }

  /*
    ----------------------------------------------------------------------------
    Singleton Stuff
    ----------------------------------------------------------------------------
  */
  private function __construct() {
    $this->loadConfiguration();
    $this->resumeSession();
    if ($this->isLoggedIn) {
      $this->loadUserData();
    }
  }

  public static function getInstance()
  {
    if (is_null(self::$instance)) {
      self::$instance = new Login();
    }
    return self::$instance;
  }

  public final function __clone()
  {
    throw new Exception('Cannot clone a singleton');
  }
}

Login::getInstance();
?>